---
title: TypeScript
description: Learn how to use TypeScript in a monorepo.
---

import { Callout } from '#/components/callout';
import { File, Folder, Files } from '#/components/files';
import { PackageManagerTabs, Tab } from '#/components/tabs';
import { LinkToDocumentation } from '#/components/link-to-documentation';

TypeScript is an excellent tool in monorepos, allowing teams to safely add types to their JavaScript code. While there is some complexity to getting set up, this guide will walk you through the important parts of a TypeScript setup for most use cases.

- [Sharing TypeScript configuration](#sharing-tsconfigjson)
- [Building a TypeScript package](#building-a-typescript-package)
- [Making type checking faster across your workspace](/repo/docs/guides/tools/typescript#linting-your-codebase)

<Callout type="info">
  This guide assumes you are using a recent version of TypeScript and uses some
  features that are only available in those versions. You may need to adjust the
  guidance on this page if you are unable to features from those versions.
</Callout>

## Sharing `tsconfig.json`

You want to build consistency into your TypeScript configurations so that your entire repo can use great defaults and your fellow developers can know what to expect when writing code in the Workspace.

TypeScript's `tsconfig.json` sets the configuration for the TypeScript compiler and features an [`extends` key](https://www.typescriptlang.org/tsconfig#extends) that you'll use to share configuration across your workspace.

This guide will use [`create-turbo`](/repo/docs/reference/create-turbo) as an example.

<PackageManagerTabs>
<Tab>

```bash title="Terminal"
npx create-turbo@latest
```

</Tab>

<Tab>

```bash title="Terminal"
yarn dlx create-turbo@latest
```

</Tab>

<Tab>

```bash title="Terminal"
pnpm dlx create-turbo@latest
```

</Tab>
</PackageManagerTabs>

### Use a base `tsconfig` file

Inside `packages/tsconfig`, we have a few `json` files which represent different ways you might want to configure TypeScript in various packages. The `base.json` file is extended by every other `package.json` in the workspace and looks like this:

```json title="./packages/tsconfig/base.json"
"compilerOptions": {
    "esModuleInterop": true,
    "skipLibCheck": true,
    "target": "es2022",
    "allowJs": true,
    "resolveJsonModule": true,
    "moduleDetection": "force",
    "isolatedModules": true,
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "module": "NodeNext"
}
```

<LinkToDocumentation href="https://www.typescriptlang.org/tsconfig">
  `tsconfig` options reference
</LinkToDocumentation>

### Creating the rest of the package

The other `tsconfig` files in this package use the `extends` key to start with the base configuration and customize for specific types of projects, like for Next.js (`nextjs.json`) and a React library (`react-library.json`).

Inside `package.json`, name the package so it can be referenced in the rest of the Workspace:

```json title="packages/tsconfig/package.json"
{
  "name": "@repo/typescript-config"
}
```

## Building a TypeScript package

### Using the configuration package

First, install the `@repo/typescript-config` package into your package:

<PackageManagerTabs>
<Tab>
```json title="./apps/web/package.json"
{
  "devDependencies": {
     "@repo/typescript-config": "*",
     "typescript": "latest",
  }
}
```
</Tab>
<Tab>
```json title="./apps/web/package.json"
{
  "devDependencies": {
     "@repo/typescript-config": "*",
     "typescript": "latest",
  }
}
```
</Tab>
<Tab>
```json title="./apps/web/package.json"
{
  "devDependencies": {
     "@repo/typescript-config": "workspace:*",
     "typescript": "latest",
  }
}
```
</Tab>
</PackageManagerTabs>

Then, extend the `tsconfig.json` for the package from the `@repo/typescript-config` package. In this example, the `web` package is a Next.js application:

```json title="./apps/web/tsconfig.json"
{
  "extends": "@repo/typescript-config/nextjs.json",
  "compilerOptions": {
    "outDir": "dist"
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}
```

### Creating entrypoints to the package

First, make sure your code gets compiled with `tsc` so there will be a `dist` directory. You'll need a `build` script as well as a `dev` script:

```json title="./packages/ui/package.json"
{
  "scripts": {
    "dev": "tsc --watch",
    "build": "tsc"
  }
}
```

Then, set up the entrypoints for your package in `package.json` so that other packages can use the compiled code:

```json title="./packages/ui/package.json"
{
  "exports": {
    "./button": {
      "types": "./src/button.ts"
      "default": "./dist/button.js",
    },
    "./input": {
      "types": "./src/input.ts"
      "default": "./dist/input.js",
    }
  }
}
```

Setting up `exports` this way has several advantages:

- Using the `types` field allows `tsserver` to use the code in `src` as the source of truth for your code's types. Your editor will always be up-to-date with the latest interfaces from your code.
- You can quickly add new entrypoints to your package without creating [dangerous barrel files](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js#what's-the-problem-with-barrel-files).
- You'll receive auto-importing suggestions for your imports across package boundaries in your editor. For more information about why you may not want to wildcard the entrypoints, see the [limitations section](/repo/docs/guides/tools/typescript#package-entrypoint-wildcards).

<Callout type="warn">
  If you're publishing the package, you cannot use references to source code in
  `types` since only the compiled code will be published to npm. You'll need to
  generate and reference declaration files and source maps.
</Callout>

## Linting your codebase

To use TypeScript as a linter, you can check the types across your workspace **fast** using Turborepo's caching and parallelization.

First, add a `check-types` script to any package that you want to check the types for:

```json title="./apps/web/package.json"
{
  "scripts": {
    "check-types": "tsc --noEmit"
  }
}
```

Then, create a `check-types` task in `turbo.json`. From the [Configuring tasks guide](/repo/docs/crafting-your-repository/configuring-tasks#dependent-tasks-that-can-be-ran-in-parallel), we can make the task run in parallel while respecting source code changes from other packages using a [Transit Node](/repo/docs/core-concepts/package-and-task-graph#transit-nodes):

```json title="./turbo.json"
{
  "tasks": {
    "topo": {
      "dependsOn": ["^topo"]
    },
    "check-types": {
      "dependsOn": ["topo"]
  },
}
```

Then, run your task using `turbo check-types`.

## Best practices

### Use `tsc` to compile your packages

For [Internal Packages](/repo/docs/core-concepts/internal-packages), we recommend that you use `tsc` to compile your TypeScript libraries whenever possible. While you can use a bundler, it's not necessary and adds extra complexity to your build process. Additionally, bundling a library can mangle the code before it makes it to your applications' bundlers, causing hard to debug issues.

### Use Node.js subpath imports instead of TypeScript compiler `paths`

It's possible to create absolute imports in your packages using [the TypeScript compiler's `paths` option](https://www.typescriptlang.org/tsconfig#paths). However, [as of TypeScript 5.4](https://devblogs.microsoft.com/typescript/announcing-typescript-5-4/#auto-import-support-for-subpath-imports), you can use [Node.js subpath imports](https://nodejs.org/api/packages.html#imports) instead.

Additionally, Node.js subpath imports are usable in [Just-in-Time Packages](/repo/docs/core-concepts/internal-packages#just-in-time-packages) while TypeScript's `compilerOptions#paths` are not.

This strategy brings your Node.js and TypeScript configurations closer together, making your project's configuration simpler.

### You likely don't need a `tsconfig.json` file in the root of your project

As mentioned in the [Structuring your repository guide](/repo/docs/crafting-your-repository/structuring-a-repository#anatomy-of-a-package), you want to treat each package in your tooling as its own unit. This means each package should have its own `tsconfig.json` to use instead of referencing a `tsconfig.json` in the root of your project. Following this practice will make it easier for Turborepo to cache your type checking tasks, simplifying your configuration.

The only case in which you may want to have a `tsconfig.json` in the Workspace root is to set configuration for TypeScript files that are not in packages. For example, if you have a script written with TypeScript that you need to run from the root, you may need a `tsconfig.json` for that file.

However, this practice is also discouraged since any changes in the Workspace root will cause all tasks to miss cache. Instead, move those scripts to a different directory in the repository.

### You likely don't need TypeScript Project References

We don't recommend using TypeScript Project References as they introduce both another point of configuration as well as another caching layer to your workspace. Both of these can cause problems in your repository with little benefit, so we suggest avoiding them when using Turborepo.

## Limitations

### Your editor won't use a package's TypeScript version

`tsserver` is not able to use different Typescript versions for different packages in your code editor. Instead, it will discover a specific version and use that everywhere.

This can result in differences between the linting errors that show in your editor and when you run `tsc` scripts to check types. If this is an issue for you, consider [keeping the TypeScript dependency on the same version](/repo/docs/crafting-your-repository/managing-dependencies#keeping-dependencies-on-the-same-version).

### Package entrypoint wildcards

We recommend [listing the entrypoints to your package explicitly](/repo/docs/guides/tools/typescript#creating-entrypoints-to-the-package) - but, to some, this feels too verbose. Instead, you can use wildcards to capture entrypoints:

```json title="./packages/ui/package.json"
{
  "exports": {
    "./*": {
      "types": "./src/*.ts",
      "default": "./dist/*.js"
    }
  }
}
```

While this will work, it comes with the tradeoff of not being able to auto-import across package boundaries [due to performance reasons with the TypeScript compiler](https://github.com/microsoft/TypeScript/issues/53116#issuecomment-1458887175). This tradeoff may or may not be worth it to you depending on your use case.
